/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lvyh.lightframe.sharding.client.aop;

import com.lvyh.lightframe.sharding.client.annotations.IdGenInfo;
import com.lvyh.lightframe.sharding.client.annotations.MultiDatasource;
import com.lvyh.lightframe.sharding.client.annotations.ShardingInfo;
import com.lvyh.lightframe.sharding.client.common.DataTableInfo;
import com.lvyh.lightframe.sharding.client.context.MultiDataSourceHolder;
import com.lvyh.lightframe.sharding.client.common.ShardModel;
import com.lvyh.lightframe.sharding.client.context.ShardingContext;
import com.lvyh.lightframe.sharding.client.idgen.IdGenStrategyFactory;
import com.lvyh.lightframe.sharding.client.strategy.ShardingStrategyFactory;
import com.lvyh.lightframe.sharding.client.util.CommonUtils;
import com.lvyh.lightframe.sharding.client.util.ComparatorUtil;
import com.lvyh.lightframe.sharding.client.util.Page;
import com.lvyh.lightframe.sharding.client.util.PageContext;
import com.lvyh.lightframe.sharding.client.util.ReflectionUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;


/**
 * Dynamic database and table aspect
 *
 * @author lvyh 2021/03/20.
 */
@Aspect
public class ShardHandlerAspect {
    private static final Logger logger = LoggerFactory.getLogger(ShardHandlerAspect.class);
    @Autowired
    private ShardingStrategyFactory shardingStrategyFactory;

    @Autowired
    private IdGenStrategyFactory idGenStrategyFactory;

    /**
     * Scan the classes decorated with @shardinginfo annotation in the classes under the specified package
     */
    @Pointcut("@within(com.lvyh.lightframe.sharding.client.annotations.ShardingInfo) && within(com.lvyh.lightframe.sharding..*)")
    public void point() {

    }

    @Around("point()")
    public Object route(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        String methodName = methodSignature.getName();
        Method targetMethod = methodSignature.getMethod();
        Class<?> targetCls = pjp.getTarget().getClass();

        //Get annotation on class
        ShardingInfo shardingInfo = targetCls.getAnnotation(ShardingInfo.class);

        MultiDatasource multiDatasource = targetCls.getAnnotation(MultiDatasource.class);
        if (multiDatasource != null) {
            MultiDataSourceHolder.set(multiDatasource.value());
            logger.info("[ShardHandlerAspect] refresh data source：{}", multiDatasource.value());
        }

        // Gets the value in the current method annotation
        IdGenInfo idGenInfo = targetMethod.getAnnotation(IdGenInfo.class);

        ShardModel shardModel = new ShardModel.Builder()
                .setShardAlg(shardingInfo.shardAlg())
                .setDbShardNum(shardingInfo.dbShardNum())
                .setTableShardNum(shardingInfo.tableShardNum())
                .setLogicDatasourceName(shardingInfo.logicDsName())
                .setLogicTableName(shardingInfo.logicTable())
                .setShardKey(shardingInfo.shardKey())
                .setIdGenTag(idGenInfo != null ? idGenInfo.idGenTag() : "")
                .setIdGenType(idGenInfo != null ? idGenInfo.idGenType() : "")
                .setPkId(idGenInfo != null ? idGenInfo.column() : "")
                .build();

        ShardingContext.set(shardModel);

        //Parameter value
        Object[] args = pjp.getArgs();
        Object parameter = args[0];
        Page page = null;

        if (methodName.startsWith("insert") || methodName.startsWith("save")) {
            //Get distributed primary key ID value
            Long id = idGenStrategyFactory.genId(idGenInfo.idGenType(), idGenInfo.idGenTag());
            ReflectionUtil.setField(parameter, idGenInfo.column(), id);
        } else if (methodName.endsWith("ByPage")) {
            int pageNum = (int) ReflectionUtil.getFieldValue(parameter, "pageNum");
            int pageSize = (int) ReflectionUtil.getFieldValue(parameter, "pageSize");
            String orderColumns = (String) ReflectionUtil.getFieldValue(parameter, "orderColumns");
            page = new Page(pageNum, pageSize, orderColumns);
        }

        //Parameter name array
        //String[] parameters = methodSignature.getParameterNames();
        if (shardingInfo.dbShardNum() > 1) {

            //shard db
            String shardingColumn = shardModel.getShardKey();
            String logicDsKey = shardModel.getLogicDatasourceName();
            String logicTableName = shardModel.getLogicTableName();
            String shardingAlg = shardModel.getShardAlg();
            int tableShardingNum = shardModel.getTableShardNum();
            int dbShardingNum = shardModel.getDbShardNum();

            Object shardingValue = ReflectionUtil.getFieldValue(parameter, shardingColumn);
            //shard key value is null
            if (shardingValue == null) {
                List<Object> result = new ArrayList<>();

                //not shard , query all db data
                for (int i = 0; i < dbShardingNum; i++) {
                    MultiDataSourceHolder.set(logicDsKey + i);
                    Object object = pjp.proceed();
                    result.addAll((List) object);
                    MultiDataSourceHolder.remove();
                }
                if (page != null) {
                    page.setTotal(result.size());
                    if (page.getTotal() > 0) {
                        if (page.getOrderBy() != null) {
                            String[] arr = page.getOrderBy().split(" ");//update_time desc
                            String orderColumn = CommonUtils.convertColumn(arr[0]);
                            boolean isAsc = arr[1].equals("asc") ? true : false;
                            Collections.sort(result, new ComparatorUtil(orderColumn, isAsc));
                        }
                        result = page.page(result);
                    }
                } else {
                    //not page
                }

                return result;
            }

            DataTableInfo dataTableInfo = this.shardingStrategyFactory.getShardInfo(shardingAlg, logicDsKey, logicTableName, shardingValue, tableShardingNum, dbShardingNum);
            MultiDataSourceHolder.set(dataTableInfo.getPhysicalDataSourceKey());
            logger.info("[ShardHandlerAspect] refresh current datasource：{}", dataTableInfo.getPhysicalDataSourceKey());

        } else if (shardingInfo.dbShardNum() == 1 && shardingInfo.tableShardNum() > 1) {
            //not shard db
            String logicDsKey = shardModel.getLogicDatasourceName();
            String defaultDsName = StringUtils.isEmpty(logicDsKey) ? MultiDataSourceHolder.getMultiDataSources().get(0) : logicDsKey;
            MultiDataSourceHolder.set(defaultDsName);
            logger.info("[ShardHandlerAspect] refresh current datasource：{}", defaultDsName);
        }

        return pjp.proceed();
    }

    @After("point()")
    public void after() {
        logger.info("[ShardHandlerAspect] clear current datasource：{}", MultiDataSourceHolder.get());
        MultiDataSourceHolder.remove();
    }

}
